/* DateType.java
 *
 * Copyright 2006, Tim Dwelle.
 *
 * Licensed under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in
 * compliance with the License.  You may obtain a copy of
 * the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in
 * writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
 * CONDITIONS OF ANY KIND, either express or implied.  See
 * the License for the specific language governing
 * permissions and limitations under the License.
 *
 */

package dowry.datatype;

import java.text.*;
import java.util.*;
import dowry.*;

/**
 * Object representation of a date datatype.
 *
 */
public class DateType extends AbstractType
{
    /**
     * Constant for the error message when validation finds
     * value that does not represent a date.
     *
     */
    public static final String ERROR_INVALID = Messages.get("ERROR_INVALID");

    /**
     * Constant for the configuration key containing the
     * datatype's <code>format</code> property indicating
     * the expected format of the date.  It may be any one
     * of the following constants:  <code>default</code>,
     * <code>short</code>, <code>medium</code>, <code>
     * long</code>, <code>full</code>.  The exact result
     * depends on the locale, but generally:
     *
     * <ul>
     *  <li>  The short format is completely numeric, such
     *        as 12.13.52
     *  </li>
     *  <li>  The medium format is longer, such as
     *        Jan 12, 1952
     *  </li>
     *  <li>  The long format is longer, such as
     *        January 12, 1952
     *  </li>
     *  <li>  The full format is pretty completely
     *        specified, such as Tuesday, April 12, 1952 AD
     *  </li>
     * </ul>
     *
     * <p>
     * The default is <code>default</code>... which is
     * equivalent to a medium format.
     * </p>
     *
     */
     public static final String FORMAT = "format";

    /**
     * Constant for the default date format.
     *
     */
    public static final String FORMAT_DEFAULT = Messages.get("default");

    /**
     * Constant for the short date format.
     *
     */
    public static final String FORMAT_SHORT = Messages.get("short");

    /**
     * Constant for the medium date format.
     *
     */
    public static final String FORMAT_MEDIUM = Messages.get("medium");

    /**
     * Constant for the long date format.
     *
     */
    public static final String FORMAT_LONG = Messages.get("long");

    /**
     * Constant for the full date format.
     *
     */
    public static final String FORMAT_FULL = Messages.get("full");

    /**
     * Constant for the datatype's name.
     *
     */
    public static final String NAME = Messages.get("date");

    /**
     * Constant for the parse/format token for am/pm
     * (default is '%am').
     *
     */
    public static final String TOKEN_AM = "%am";

    /**
     * Constant for the parse/format token for day in month
     * (default is '%d').
     *
     */
    public static final String TOKEN_D = "%d";

    /**
     * Constant for the parse/format token for day of week
     * (default is '%dayOfWeek').
     *
     */
    public static final String TOKEN_DAY_OF_WEEK = "%dayOfWeek";

    /**
     * Constant for the parse/format token for day in month
     * padded to 2 digits (default is '%dd').
     *
     */
    public static final String TOKEN_DD = "%dd";

    /**
     * Constant for the parse/format token for day of week
     * as an abbreviated string (default is '%dow').
     *
     */
    public static final String TOKEN_DOW = "%dow";

    /**
     * Constant for the parse/format token for hour in am/pm
     * 1-12 (default is '%h').
     *
     */
    public static final String TOKEN_H = "%h";

    /**
     * Constant for the parse/format token for hour in day
     * 0-23 (default is '%h24').
     *
     */
    public static final String TOKEN_H24 = "%h24";

    /**
     * Constant for the parse/format token for hour in am/pm
     * 01-12 padded to 2 digits (default is '%hh').
     *
     */
    public static final String TOKEN_HH = "%hh";

    /**
     * Constant for the parse/format token for hour in day
     * 00-23 padded to 2 digits (default is '%hh24').
     *
     */
    public static final String TOKEN_HH24 = "%hh24";

    /**
     * Constant for the parse/format token for a literal
     * representation of the token start (default is '%').
     *
     */
    public static final char TOKEN_LITERAL = '%';

    /**
     * Constant for the parse/format token for month in year
     * 1-12 (default is '%m').
     *
     */
    public static final String TOKEN_M = "%m";

    /**
     * Constant for the parse/format token for minutes
     * in hour 00-59 padded to 2 digits (default is '%mi').
     *
     */
    public static final String TOKEN_MI = "%mi";

    /**
     * Constant for the parse/format token for month in year
     * 01-12 padded to 2 digits (default is '%m').
     *
     */
    public static final String TOKEN_MM = "%mm";

    /**
     * Constant for the parse/format token for month in year
     * as an abbreviated string (default is '%mon').
     *
     */
    public static final String TOKEN_MON = "%mon";

    /**
     * Constant for the parse/format token for month in year
     * as full string (default is '%month').
     *
     */
    public static final String TOKEN_MONTH = "%month";

    /**
     * Constants for the parse/format token for a single
     * space.  While a space is a valid literal, this
     * provides a convenient way to express a space in the
     * CSS classname  (default is '%_').
     *
     */
    public static final String TOKEN_SPACE = "%_";

    /**
     * Constant for the parse/format token for seconds in
     * minute 00-59 padded to 2 digits (default is '%ss').
     *
     */
    public static final String TOKEN_SS = "%ss";

    /**
     * Constant for the parse/format token for 2 digit year.
     * For parsing, this is interpretted to a full year by
     * adjusting dates to be within 80 years before and 20
     * years after the current year.
     *
     */
    public static final String TOKEN_YY = "%yy";

    /**
     * Constant for the parse/format token for full year
     * (default is '%yyyy').
     *
     */
    public static final String TOKEN_YYYY = "%yyyy";

    /**
     * Constant for the appropriate order for parse/format
     * tokens to be processed.
     *
     */
    private static final String[] TOKENS = { "%_",
                                             "%am",
                                             "%dayOfWeek",
                                             "%dow",
                                             "%dd",
                                             "%d",
                                             "%hh24",
                                             "%h24",
                                             "%hh",
                                             "%h",
                                             "%month",
                                             "%mon",
                                             "%mm",
                                             "%mi",
                                             "%m",
                                             "%ss",
                                             "%yyyy",
                                             "%yy" };

    /*
     * Constants for the equivalent SimpleDateFormat tokens
     * that correspond to the DateType tokens.
     *
     */
    private static final String SDF_TOKEN_AM          = "aa";
    private static final String SDF_TOKEN_D           = "d";
    private static final String SDF_TOKEN_DAY_OF_WEEK = "EEEEE";
    private static final String SDF_TOKEN_DD          = "dd";
    private static final String SDF_TOKEN_DOW         = "EEE";
    private static final String SDF_TOKEN_H           = "h";
    private static final String SDF_TOKEN_H24         = "H";
    private static final String SDF_TOKEN_HH          = "hh";
    private static final String SDF_TOKEN_HH24        = "HH";
    private static final String SDF_TOKEN_M           = "M";
    private static final String SDF_TOKEN_MI          = "mm";
    private static final String SDF_TOKEN_MM          = "MM";
    private static final String SDF_TOKEN_MON         = "MMM";
    private static final String SDF_TOKEN_MONTH       = "MMMMM";
    private static final String SDF_TOKEN_SPACE       = " ";
    private static final String SDF_TOKEN_SS          = "ss";
    private static final String SDF_TOKEN_YY          = "yy";
    private static final String SDF_TOKEN_YYYY        = "yyyy";
    private static final char   SDF_QUOTE_LITERAL     = '\'';

    /**
     * The parse/format mask to use with the full date
     * format.
     *
     */
    protected String fullPattern = Messages.get("dateFullPattern");

    /**
     * The parse/format mask to use with the long date
     * format.
     *
     */
    protected String longPattern = Messages.get("dateLongPattern");

    /**
     * The parse/format mask to use with the medium date
     * format.
     *
     */
    protected String mediumPattern = Messages.get("dateMediumPattern");

    /**
     * The parse/format mask to use with the short date
     * format.
     *
     */
    protected String shortPattern = Messages.get("dateShortPattern");

    /**
     * Gets the name of the Javascript class that serves
     * as the client side equivalent to this Java Datatype.
     *
     * @return  the string Javascript class name of the
     *          datatype
     *
     */
    public String getJsClass()
    {
        return DateType.class.getName();
    }

    /**
     * Tests if this datatype is a handler for the
     * specified Dowry type name.
     *
     * @param  typeName  the Dowry type name
     *
     * @return           true if this datatype can handle
     *                   the specified type name; false
     *                   otherwise
     *
     */
    public boolean isType(String typeName)
    {
        return (typeName != null && typeName.equals(NAME));
    }

    /**
     * Converts the Java class for this datatype to the type
     * representation used by Dowry on the client.
     *
     * <p>
     * This implementation returns 'date'.
     * </p>
     *
     * @param  c   the class to get the type name for
     *
     * @return     the string representation of type to be
     *             used in Dowry
     *
     */
    public String toType(Class c)
    {
        return NAME;
    }

    /**
     * Validates that:
     *
     * <ol>
     *  <li>  the value is provided, if required as
     *        specified by map key <code>required</code>
     *  </li>
     *  <li>  the value represents a valid date
     *  </li>
     * </ol>
     *
     * @param  cfg  a map containing all the configuration
     *              information relevant to the local
     *              representation of this datatype
     *
     * @return      the string error message, if the
     *              input is invalid; otherwise, null
     *
     */
    public String validate(Map cfg)
    {
        // call super.validate()...
        String msg = super.validate(cfg);

        String val = getString(cfg, VALUE);
        if (msg == null && val != null)
        {
            // determine the style for this date format
            String formatName = getString(cfg, FORMAT);
            String pattern = getFormat(formatName);

            try
            {
                // TODO: use a locale, too?
                SimpleDateFormat sdf = new SimpleDateFormat(pattern);
                sdf.parse(val);
            }
            catch(ParseException pex)
            {
                msg = ERROR_INVALID;
            }
        }

        return msg;
    }

    /**
     * Gets the simple date format accounting for any
     * literal parse/format mask.
     *
     * @param format  the raw format
     *
     * @return        the simple date format pattern
     *
     */
    protected String getFormat(String format)
    {
        // deal with any 'named' formats
        if (format == null ||
            format.equals(FORMAT_DEFAULT) ||
            format.equals(FORMAT_MEDIUM))
        {
            format = mediumPattern;
        }
        if (format.equals(FORMAT_SHORT))
        {
            format = shortPattern;
        }
        else if (format.equals(FORMAT_LONG))
        {
            format = longPattern;
        }
        else if (format.equals(FORMAT_FULL))
        {
            format = fullPattern;
        }

        // capture our format tokens
        // in the string buffer
        StringBuffer tokens = new StringBuffer();

        int start = 0;
        int len = format.length();
        for (int i=0; i < len; i++)
        {
            // character represents the start of
            // a token
            char c = format.charAt(i);

            if (c == TOKEN_LITERAL ||
                c == SDF_QUOTE_LITERAL)
            {
                // add any literal text between
                // last token and this token...
                if (start < i)
                {
                    String s = format.substring(start, i);

                    tokens.append(SDF_QUOTE_LITERAL);
                    tokens.append(s);
                    tokens.append(SDF_QUOTE_LITERAL);
                }

                if (c == TOKEN_LITERAL)
                {
                    // loop through all explicit tokens
                    for (int j=0; j < this.TOKENS.length; j++)
                    {
                        String token = this.TOKENS[j];
                        if (format.indexOf(token, i) == i)
                        {
                            String pattern = null;
                            if (token.equals(TOKEN_AM))
                            {
                                pattern = SDF_TOKEN_AM;
                            }
                            else if (token.equals(TOKEN_D))
                            {
                                pattern = SDF_TOKEN_D;
                            }
                            else if (token.equals(TOKEN_DD))
                            {
                                pattern = SDF_TOKEN_DD;
                            }
                            else if (token.equals(TOKEN_DAY_OF_WEEK))
                            {
                                pattern = SDF_TOKEN_DAY_OF_WEEK;
                            }
                            else if (token.equals(TOKEN_DOW))
                            {
                                pattern = SDF_TOKEN_DOW;
                            }
                            else if (token.equals(TOKEN_H))
                            {
                                pattern = SDF_TOKEN_H;
                            }
                            else if (token.equals(TOKEN_H24))
                            {
                                pattern = SDF_TOKEN_H24;
                            }
                            else if (token.equals(TOKEN_HH))
                            {
                                pattern = SDF_TOKEN_HH;
                            }
                            else if (token.equals(TOKEN_HH24))
                            {
                                pattern = SDF_TOKEN_HH24;
                            }
                            else if (token.equals(TOKEN_M))
                            {
                                pattern = SDF_TOKEN_M;
                            }
                            else if (token.equals(TOKEN_MI))
                            {
                                pattern = SDF_TOKEN_MI;
                            }
                            else if (token.equals(TOKEN_MM))
                            {
                                pattern = SDF_TOKEN_MM;
                            }
                            else if (token.equals(TOKEN_MON))
                            {
                                pattern = SDF_TOKEN_MM;
                            }
                            else if (token.equals(TOKEN_MONTH))
                            {
                                pattern = SDF_TOKEN_MONTH;
                            }
                            else if (token.equals(TOKEN_SPACE))
                            {
                                pattern = SDF_TOKEN_SPACE;
                            }
                            else if (token.equals(TOKEN_SS))
                            {
                                pattern = SDF_TOKEN_SS;
                            }
                            else if (token.equals(TOKEN_YY))
                            {
                                pattern = SDF_TOKEN_YY;
                            }
                            else if (token.equals(TOKEN_YYYY))
                            {
                                pattern = SDF_TOKEN_YYYY;
                            }
                            else
                            {
                                pattern = "" + TOKEN_LITERAL;
                            }

                            if (pattern != null)
                            {
                                tokens.append(pattern);
                                i = i + token.length() - 1;
                                break;
                            }
                        }
                    }
                }
                else if (c == SDF_QUOTE_LITERAL)
                {
                    // if we have more literal text, then
                    // we don't have to worry... since the
                    // end quote and the start quote will
                    // produce our escape quote sequence
                    // for the omitted quote literal... so
                    // we only need to do something if we
                    // are at either end of the format
                    // string, or if we are about to
                    // display a pattern token
                    if (i == 0 ||
                        i+1 == len ||
                        (i+1 < len &&
                         format.charAt(i+1) == TOKEN_LITERAL))
                    {
                        tokens.append(SDF_QUOTE_LITERAL);
                        tokens.append(SDF_QUOTE_LITERAL);
                    }
                }

                // advance the start token so
                // it matches our counter when
                // we begin the next iteration
                start = i+1;
            }
        }

        // any remainder represents a final
        // literal token...
        if (start < len)
        {
            String s = format.substring(start);

            tokens.append(SDF_QUOTE_LITERAL);
            tokens.append(s);
            tokens.append(SDF_QUOTE_LITERAL);
        }

        return tokens.toString();
    }
}